<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Gridstack.js React integration example</title>
  <link rel="stylesheet" href="demo.css" />
  <link rel="stylesheet" href="../dist/gridstack-extra.css"/>
  <script src="../dist/gridstack-all.js"></script>

  <!-- Scripts to use react inside html - DEVELOPMENT FILES -->
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script>
</head>

<body>
  <div>
    <h2>Controlled stack</h2>
    <div id="controlled-stack"></div>
  </div>
</body>

<script type="text/babel">
  /***********************************************************************************************************/
  /********************************* NOT IDEAL - see comments below line ~96) ********************************/
  /***********************************************************************************************************/

  const { useState, useEffect, useLayoutEffect, createRef, useRef } = React
  const Item = ({ id }) => <div>{id}</div>

  //
  // Controlled example
  //
  const ControlledStack = ({ items, addItem, changeItems }) => {
    const refs = useRef({})
    const gridRef = useRef()
    const gridContainerRef = useRef(null)
    refs.current = {}

    if (Object.keys(refs.current).length !== items.length) {
      items.forEach(({ id }) => {
        refs.current[id] = refs.current[id] || createRef()
      })
    }

    useLayoutEffect(() => {
      if (!gridRef.current) {
        // no need to init twice (would will return same grid) or register dup events
        const grid = gridRef.current = GridStack.init(
          {
            float: false,
            acceptWidgets: true,
            column: 6,
            minRow: 1,
          },
          gridContainerRef.current
        )
        .on('added', (ev, gsItems) => {
          if (grid._ignoreCB) return;
          // remove the new element as React will re-create it again (dup) once we add to the list or we get 2 of them with same ids but different DOM el!
          // TODO: this is really not ideal - we shouldn't mix React templating with GS making it own edits as those get out of sync! see comment below ~96.
          gsItems.forEach(n => {
            grid.removeWidget(n.el, true, false); // true=remove DOM, false=don't call use back!
            // can't pass n directly even though similar structs as it has n.el.gridstackNode which gives JSON error for circular write.
            addItem({id:n.id, x:n.x, y:n.y, w:n.w, h:n.h});
          });
        })
        .on('removed change', (ev, gsItems) => {
          // synch our version from GS....
          // Note: we could just update those few items passed but save() is fast and it's easier to just set an entire new list
          // and since we have the same ids, React will not re-create anything...
          const newItems = grid.save(false); // saveContent=false
          changeItems(newItems);
        })
        // addEvents(grid, i);
      } else {
        //
        // update existing GS layout, which is optimized to updates only diffs and add new/delete items as well
        //
        const grid = gridRef.current;
        const layout = items.map((a) => 
          // use exiting nodes (which will skip diffs being the same) else new elements Widget but passing the React dom .el so we know what to makeWidget() on!
          refs.current[a.id].current.gridstackNode || {...a, el: refs.current[a.id].current}
        );
        grid._ignoreCB = true; // hack: ignore added/removed since we're the one doing the update
        grid.load(layout);
        delete grid._ignoreCB;
      }

    }, [items])

    return (
      // ********************
      // NOTE: constructing DOM grid items in template when gridstack is also allowed creating (dragging between grids, or adding/removing from say a toolbar)
      // is NOT A GOOD IDEA as you end up fighting between gridstack users' edits and your template items structure which are not in sync.
      // At best, you end up re-creating widgets DOM (from React template) and all their content & state after a widget was inserted/re-parented by the user.
      // a MUCH better way is to let GS create React components using it's API/user interactions, with only initial load() of a stored layout.
      // See the Angular component wrapper that does that: https://github.com/gridstack/gridstack.js/tree/master/angular/ (lib author uses Angular)
      // ...TBD creating React equivalent...
      //
      // Also templating forces you to spell out the 15+ GridStackWidget attributes (only x,y,w,h done below), instead of passing an option structure that 
      // supports everything, is not robust as things get added and pollutes the DOM attr for default/missing entries, vs the optimized code in GS.
      // ********************
      <div style={{ width: '100%', marginRight: '10px' }}>
        <div className="grid-stack" ref={gridContainerRef}>
          {items.map((item, i) => {
            return (
              <div ref={refs.current[item.id]} key={item.id} className="grid-stack-item" gs-id={item.id} gs-w={item.w} gs-h={item.h} gs-x={item.x} gs-y={item.y}>
                <div className="grid-stack-item-content">
                  <Item {...item} />
                </div>
              </div>
            )
          })}
        </div>
        <code>
          <pre>{JSON.stringify(items, null, 2)}</pre>
        </code>
      </div>
    )
  }

  const ControlledExample = () => {
    const [items1, setItems1] = useState([{ id: 'item-1-1', x: 0, y: 0, w: 2, h: 2 }, { id: 'item-1-2', x: 2, y: 0, w: 2, h: 2 }])
    const [items2, setItems2] = useState([{ id: 'item-2-1', x: 0, y: 0 }, { id: 'item-2-2', x: 0, y: 1 }, { id: 'item-2-3', x: 1, y: 0 }])

    return (
      <div>
        <div style={{display: 'flex', gap: '16px', marginBottom: '16px'}}>
          <div></div>
        </div>

        <div style={{ display: 'flex', gap: '16px', marginBottom: '16px' }}>
          <button onClick={() => setItems1(items => [...items, { id: `item-1-${Date.now()}`, x: 2, y: 0, w: 2, h: 2 }])}>Add Item to 1 grid</button>
          <button onClick={() => setItems2(items => [...items, { id: `item-2-${Date.now()}`, x: 2, y: 0, w: 2, h: 2 }])}>Add Item to 2 grid</button>
        </div>
        <div style={{display: 'flex'}}>
          <div style={{ display: 'flex', width: '50%' }}>
            <ControlledStack
              items={items1}
              addItem={(item) => {
                setItems1(items => [...items, item])
              }}
              changeItems={(items) => setItems1(items)}
            />
          </div >
          <div style={{ display: 'flex', width: '50%' }}>
            <ControlledStack
              items={items2}
              addItem={(item) => {
                setItems2(items => [...items, item])
              }}
              changeItems={(items) => setItems2(items)}
            />
          </div>
        </div >
      </div>
    )
  }

  ReactDOM.render(<ControlledExample />, document.getElementById('controlled-stack'))
</script>
</html>